package engine.scene;

import static org.lwjgl.opengl.GL11.*;

import java.nio.IntBuffer;
import java.util.ArrayList;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;

import engine.collider.CollisionDetector;
import engine.collider.ContactPoint;
import engine.core.OpenGLView;
import engine.gameObject.Camera;
import engine.gameObject.DynamicObject;
import engine.gameObject.GameObject;
import engine.gameObject.camera.DefaultCamera;
import engine.gameObject.listener.KeyListener;
import engine.gameObject.listener.MouseListener;
import engine.gameObject.listener.MouseMotionListener;
import engine.gameObject.listener.MouseWheelListener;
import engine.gui.GUIStyle;
import engine.gui.WindowManager;
import engine.light.Light;
import engine.light.LightManager;
import engine.scene.tree.Octree;
import engine.util.ortho.Draw2D;
import engine.util.shader.Shader;
import engine.util.shader.ShaderLoader;

/**
 * This class is for containing a scene of the game. One scene can have its objects and a hud.  
 * @author Richard
 *
 */
public abstract class Scene {
	public static final float OCTREE_MIN_SIZE = 20;
	
	//The current camera. 
	private Camera currentCamera;
	
	//The core. 
	private OpenGLView openGLView;
	
	//A window manager for handling the hud. 
	private WindowManager windowManager;
	
	//The light manager to handle the limited light sources of opengl
	private LightManager lightManager;
	
	//The name of the scene. This name should be unique for correct adressing. 
	private String name;
	
	//The two frustums. 
	private static Frustum nearFrustum = new Frustum();
	private static Frustum farFrustum = new Frustum();
	
	//Lists for initializing the tree.
	private ArrayList<GameObject> nearList;
	private ArrayList<GameObject> farList;
	
	//Lists
	private Octree nearOctree;
	private Octree farOctree;
	
	//FBO's for rendering the different planes. 
	private FBORenderer nearFBO;
	private FBORenderer farFBO;
	private FBORenderer skyFBO;
	
	//Address to the texture to screen shader. 
	private int renderTexToScreenShader;
	
	//A sky object, that will always be behind the scene. 
	private GameObject skyObject;
	
	//Lists for the listeners. Game objects with those listeners are added to these lists. 
	private ArrayList<KeyListener> keyListeners;
	private ArrayList<MouseListener> mouseListeners;
	private ArrayList<MouseMotionListener> mouseMotionListeners;
	private ArrayList<MouseWheelListener> mouseWheelListeners;
	
	public Scene(String name) {
		this.name = name;
		
		nearList = new ArrayList<GameObject>();
		farList = new ArrayList<GameObject>();
		
		keyListeners = new ArrayList<KeyListener>();
		mouseListeners = new ArrayList<MouseListener>();
		mouseMotionListeners = new ArrayList<MouseMotionListener>();
		mouseWheelListeners = new ArrayList<MouseWheelListener>();
		
		currentCamera = new DefaultCamera();
	}
	
	public void addGameObject(GameObject gameObject) {
		gameObject.setScene(this);
		
		if(gameObject.isInNearPane()) {
			nearList.add(gameObject);
		} else {
			farList.add(gameObject);
		}
		
		if(gameObject.isCollidable()) {
			//TODO add to collision octree
		}
		
		if(gameObject instanceof KeyListener) {
			keyListeners.add((KeyListener) gameObject);
		}
		
		if(gameObject instanceof MouseListener) {
			mouseListeners.add((MouseListener) gameObject);
		}
		
		if(gameObject instanceof MouseMotionListener) {
			mouseMotionListeners.add((MouseMotionListener) gameObject);
		}
		
		if(gameObject instanceof MouseWheelListener) {
			mouseWheelListeners.add((MouseWheelListener) gameObject);
		}
	}
	
	public void addLight(Light light) {
		lightManager.add(light);
	}
	
	public void removeLight(Light light) {
		lightManager.remove(light);
	}
	
	public final void initAbs() {
		lightManager = new LightManager(this);
		
		IntBuffer buffer = BufferUtils.createIntBuffer(16);
		GL11.glGetInteger(GL11.GL_VIEWPORT, buffer);
		windowManager = new WindowManager(buffer.get(2), buffer.get(3));
		buffer.clear();
		
		init();
		
		//compute the min and max bounds of the octrees
		double[] min = new double[3];
		double[] max = new double[3];
		for(int i = 0; i < 3; i++) {
			min[i] = Double.MAX_VALUE;
			max[i] = Double.MIN_VALUE;
		}
		
		for(GameObject g: nearList) {
			for(int i = 0; i < 3; i++) {
				min[i] = Math.min(min[i], g.getPosition()[i] + g.getBoundingSphere().getPosition()[i] - g.getBoundingSphere().getRadius());
				max[i] = Math.max(max[i], g.getPosition()[i] + g.getBoundingSphere().getPosition()[i] + g.getBoundingSphere().getRadius());
			}
		}
		
		for(GameObject g: farList) {
			for(int i = 0; i < 3; i++) {
				min[i] = Math.min(min[i], g.getPosition()[i] + g.getBoundingSphere().getPosition()[i] - g.getBoundingSphere().getRadius());
				max[i] = Math.max(max[i], g.getPosition()[i] + g.getBoundingSphere().getPosition()[i] + g.getBoundingSphere().getRadius());
			}
		}
		
		//create octrees
		nearOctree = new Octree(nearFrustum, min, max, OCTREE_MIN_SIZE);
		farOctree = new Octree(farFrustum, min, max, OCTREE_MIN_SIZE);
		
		//fill objects into octree
		for(GameObject g: nearList) {
			nearOctree.insert(g);
		}
		
		for(GameObject g: farList) {
			farOctree.insert(g);
		}
		
		//get the maximum size of textures
		DisplayMode displayMode = Display.getDesktopDisplayMode();
		int texWidth = 1;
		while(texWidth < displayMode.getWidth()) {
			texWidth *= 2;
		}
		int texHeight = 1;
		while(texHeight < displayMode.getHeight()) {
			texHeight *= 2;
		}
		
		texWidth = Math.min(texWidth, glGetInteger(GL_MAX_TEXTURE_SIZE));
		texHeight = Math.min(texHeight, glGetInteger(GL_MAX_TEXTURE_SIZE));
		
		//create fbos. 
		nearFBO = new FBORenderer(texWidth, texHeight, false);
		farFBO = new FBORenderer(texWidth, texHeight, false);
		skyFBO = new FBORenderer(texWidth, texHeight, false);
		
		//load shader for fbos
		renderTexToScreenShader = ShaderLoader.getShader("shader/engine/texToScreen.vert", "shader/engine/texToScreen.frag");
		Shader.useShader(renderTexToScreenShader);
		Shader.sendUniform1i(renderTexToScreenShader, "tex", 0);
		Shader.sendUniform1i(renderTexToScreenShader, "depth", 1);
		Shader.sendUniform1f(renderTexToScreenShader, "maxDepth", 1);
		Shader.disableShader();
	}
	
	public void init() {}

	public final void updateView(int width, int height) {
		windowManager.updateView(width, height);

		currentCamera.setWidth(width);
		currentCamera.setHeight(height);
	}
	
	public void exit() {}
	
	public void setCurrent() {}
	
	public void unsetCurrent() {}
	
	public final void renderAbs(float timeElapsed) {
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		glLoadIdentity();
		
		//update camera and extract frustum
		currentCamera.update(timeElapsed);
			
		currentCamera.prepareNearPane();
		currentCamera.look();
		nearFrustum.update();
		lightManager.applyLights();
		glLoadIdentity();
			
		currentCamera.prepareFarPane();
		currentCamera.look();
		farFrustum.update();
		glLoadIdentity();
		
		//reset the octrees
		nearOctree.reset();
		farOctree.reset();
		
		//at first, update any dynamic object
		nearOctree.update(timeElapsed);
		farOctree.update(timeElapsed);
		
		//handle collision detection
		ArrayList<DynamicObject> dynamicObjects = new ArrayList<DynamicObject>();
		dynamicObjects.addAll(nearOctree.getDynamicObjects());
		dynamicObjects.addAll(farOctree.getDynamicObjects());
		checkCollision(timeElapsed, dynamicObjects);
		/*//1 compute collision from dynamic with static objects. 
		//be careful only to check any collision once
		ArrayList<Leaf> leaves = new ArrayList<Leaf>();
		for(DynamicObject d: nearOctree.getDynamicObjects()) {
			if(d.isCollidable()) {
				nearOctree.getContactLeaves(leaves, d);
				for(Leaf l: leaves) {
					for(StaticObject s: l.getStaticObjects()){
						if(s.isCollidable()) {
							CollisionDetector.collide(d, s);
						}
					}
				}
			}
			
			leaves.clear();
		}
		
		//2 compute collision from any dynamic object with any other dynamic object
		//check here also only once.
		for(DynamicObject d1: nearOctree.getDynamicObjects()) {
			for(DynamicObject d2: nearOctree.getDynamicObjects()) {
				if(d1 != d2) {
					CollisionDetector.collide(d1, d2);
				}
			}
		}*/
		
		//render sky into fbo and render texture
		renderSkyObject(timeElapsed);
		
		//render farpane into fbo and render texture
		renderFarPane(timeElapsed);
		
		//render nearpane into fbo and render texture
		renderNearPane(timeElapsed);
		
		//renderes the prepared fbos to the screen
		renderTexturesToScreen();
		
		render(timeElapsed);
		
		windowManager.render();
	}
	
	private void checkCollision(double timeElapsed, ArrayList<DynamicObject> dynamicObjects) {
		ArrayList<GameObject> staticObjects = new ArrayList<GameObject>();
		ContactPoint contactPoint = null;
		
		//find the first collision, that occurs
		//for each dynamic object, check collision
		for(int i = 0; i < dynamicObjects.size(); i++) {
			//Check collision with other dynamic objects
			for(int j = i + 1; j < dynamicObjects.size(); j++) {
				contactPoint = CollisionDetector.getMin(contactPoint, CollisionDetector.collide(dynamicObjects.get(i), dynamicObjects.get(j), timeElapsed));
			}
			
			//check collision with static objects
			nearOctree.getNearObjects(staticObjects, dynamicObjects.get(i));
			farOctree.getNearObjects(staticObjects, dynamicObjects.get(i));
				
			for(GameObject s: staticObjects) {
				contactPoint = CollisionDetector.getMin(contactPoint, CollisionDetector.collide(dynamicObjects.get(i), s, timeElapsed));
			}
		}
		
		if(contactPoint != null) {
			double moveTime = timeElapsed * contactPoint.getDistance();
			double timeLeft = timeElapsed - moveTime;
			
//			System.out.println(moveTime);
			
			for(DynamicObject d: dynamicObjects) {
				d.move(moveTime);
			}
			
			GameObject g1 = contactPoint.getGameObject();
			GameObject g2 = contactPoint.getCollider();
			
			//collision of g1
			contactPoint.setGameObject(g2);
			g1.collided(contactPoint, timeLeft);
			
			//invert contact vector and collision of g2
			contactPoint.setGameObject(g1);
			contactPoint.setVx(-contactPoint.getVx());
			contactPoint.setVy(-contactPoint.getVy());
			contactPoint.setVz(-contactPoint.getVz());
			g2.collided(contactPoint, timeLeft);
			
			//recursive call
			checkCollision(timeLeft, dynamicObjects);
		} else {
			for(DynamicObject d: dynamicObjects) {
				d.move(timeElapsed);
			}
		}
	}

	private void renderSkyObject(float timeElapsed) {
		glDisable(GL_LIGHTING);
		skyFBO.preRender();
		
		if(skyObject != null) {
			currentCamera.prepareFarPane();
			currentCamera.look();
			skyObject.setPosition(currentCamera.getEyeX(), currentCamera.getEyeY(), currentCamera.getEyeZ());
			skyObject.renderAbs(timeElapsed);
			skyObject.setRendered(false);
		}
		
		skyFBO.postRender();
		glEnable(GL_LIGHTING);
	}
	
	private void renderFarPane(float timeElapsed) {
		farFBO.preRender();
		
		currentCamera.prepareFarPane();
		currentCamera.look();
		
		farOctree.render(timeElapsed);
		
		farFBO.postRender();
	}
	
	private void renderNearPane(float timeElapsed) {
		nearFBO.preRender();
		
		currentCamera.prepareNearPane();
		currentCamera.look();
		
		nearOctree.render(timeElapsed);
		
		nearFBO.postRender();
	}
	
	private void renderTexturesToScreen() {
		glDisable(GL_LIGHTING);
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		
		glMatrixMode(GL_PROJECTION);
		glPushMatrix();
		{
			glLoadIdentity();
			glOrtho(0,windowManager.getWidth(), 0, windowManager.getHeight(), 0, 1);
			glMatrixMode(GL_MODELVIEW);
			glPushMatrix();
			{
				glLoadIdentity();
				
				//draw sky fbo
				GL13.glActiveTexture(GL13.GL_TEXTURE0);
				glEnable(GL_TEXTURE_2D);
				glBindTexture(GL_TEXTURE_2D, skyFBO.getTexID());
				
				glColor4f(1, 1, 1, 1);
				Draw2D.drawImage(0, 0, skyFBO.getWidth(), skyFBO.getHeight(), 0, 1, 1, -1, -1f);
				
				Shader.useShader(renderTexToScreenShader);
				
				//draw far fbo
				GL13.glActiveTexture(GL13.GL_TEXTURE0);
				glBindTexture(GL_TEXTURE_2D, farFBO.getTexID());
				
				GL13.glActiveTexture(GL13.GL_TEXTURE1);
				glBindTexture(GL_TEXTURE_2D, farFBO.getDepthTexID());
				
				Shader.sendUniform1f(renderTexToScreenShader, "maxDepth", 1);
				Draw2D.drawImage(0, 0, farFBO.getWidth(), farFBO.getHeight(), 0, 1, 1, -1);
				
				//draw near fbo
				GL13.glActiveTexture(GL13.GL_TEXTURE0);
				glBindTexture(GL_TEXTURE_2D, nearFBO.getTexID());
				
				GL13.glActiveTexture(GL13.GL_TEXTURE1);
				glBindTexture(GL_TEXTURE_2D, nearFBO.getDepthTexID());
				
				Shader.sendUniform1f(renderTexToScreenShader, "maxDepth", (float)currentCamera.getNearPane() / currentCamera.getFarPane());
				
				Draw2D.drawImage(0, 0, nearFBO.getWidth(), nearFBO.getHeight(), 0, 1, 1, -1);
				
				//disable textures
				GL13.glActiveTexture(GL13.GL_TEXTURE1);
				glBindTexture(GL_TEXTURE_2D, 0);
				
				GL13.glActiveTexture(GL13.GL_TEXTURE0);
				glBindTexture(GL_TEXTURE_2D, 0);
				
				Shader.disableShader();
			}
			glPopMatrix();
			glMatrixMode(GL_PROJECTION);
		}
		glPopMatrix();
		glMatrixMode(GL_MODELVIEW);
		
		glDisable(GL_BLEND);
		glEnable(GL_LIGHTING);
	}
	
	public void render(float timeElapsed){};
	
	public void keyPressed(int key){}
	
	public final void keyPressedAbs(int key) {
		if(windowManager.keyPressed(key)) {
			return;
		}
		
		currentCamera.keyPressed(key);
		
		for(KeyListener keyListener: keyListeners) {
			keyListener.keyPressed(key);
		}
		
		keyPressed(key);
	}
	
	public void keyReleased(int key){}
	
	public final void keyReleasedAbs(int key) {
		if(windowManager.keyReleased(key)) {
			return;
		}
		
		currentCamera.keyReleased(key);
		
		for(KeyListener keyListener: keyListeners) {
			keyListener.keyReleased(key);
		}
		
		keyReleased(key);
	}
	
	public void mousePressed(int posX, int posY, int button) {}
	
	public final void mousePressedAbs(int posX, int posY, int button) {
		if(windowManager.mousePressed(posX, posY, button)) {
			return;
		}
		
		currentCamera.mousePressed(posX, posY, button);
		
		for(MouseListener mouseListener: mouseListeners) {
			mouseListener.mousePressed(posX, posY, button);
		}
		
		mousePressed(posX, posY, button);
	}
	
	public void mouseReleased(int posX, int posY, int button) {}
	
	public final void mouseReleasedAbs(int posX, int posY, int button) {
		if(windowManager.mouseReleased(posX, posY, button)) {
			return;
		}
		
		currentCamera.mouseReleased(posX, posY, button);
		
		for(MouseListener mouseListener: mouseListeners) {
			mouseListener.mouseReleased(posX, posY, button);
		}
		
		mouseReleased(posX, posY, button);
	}
	
	public void mouseMoved(int posX, int posY, int button) {}
	
	public final void mouseMovedAbs(int posX, int posY, int button) {
		if(windowManager.mouseMoved(posX, posY, button)) {
			return;
		}
		
		currentCamera.mouseMoved(posX, posY, button);
		
		for(MouseMotionListener mouseMotionListener: mouseMotionListeners) {
			mouseMotionListener.mouseMoved(posX, posY, button);
		}
		
		mouseMoved(posX, posY, button);
	}
	
	public void mouseDragged(int posX, int posY, int button) {}
	
	public final void mouseDraggedAbs(int posX, int posY, int button) {
		if(windowManager.mouseDragged(posX, posY, button)) {
			return;
		}

		currentCamera.mouseDragged(posX, posY, button);
		
		for(MouseMotionListener mouseMotionListener: mouseMotionListeners) {
			mouseMotionListener.mouseDragged(posX, posY, button);
		}
		
		mouseDragged(posX, posY, button);
	}
	
	public void mouseWheelMoved(int movement) {}
	
	public final void mouseWheelMovedAbs(int movement) {
		if(windowManager.mouseWheelMoved(movement)) {
			return;
		}

		currentCamera.mouseWheelMoved(movement);
		
		for(MouseWheelListener mouseWheelListener: mouseWheelListeners) {
			mouseWheelListener.mouseWheelMoved(movement);
		}
		
		mouseWheelMoved(movement);
	}
	
	public WindowManager getWindowManager() {
		return windowManager;
	}
	
	public String getName() {
		return name;
	}
	
	public void setName(String name) {
		this.name = name;
	}

	public void setOpenGLView(OpenGLView openGLView) {
		this.openGLView = openGLView;
	}
	
	public void setCurrentScene(String scene) {
		openGLView.setCurrentScene(scene);
	}

	public Camera getCurrentCamera() {
		return currentCamera;
	}

	public void setCurrentCamera(Camera currentCamera) {
		currentCamera.setWidth(windowManager.getWidth());
		currentCamera.setHeight(windowManager.getHeight());
		this.currentCamera = currentCamera;
	}

	public GameObject getSkyObject() {
		return skyObject;
	}

	public void setSkyObject(GameObject skyObject) {
		this.skyObject = skyObject;
	}
	
	public void setGUIStyle(GUIStyle guiStyle) {
		windowManager.setGuiStyle(guiStyle);
	}
	
	public GUIStyle getGUIStyle() {
		return windowManager.getGuiStyle();
	}
}
